OpenAIとStreamlitで画像を英語で描写する練習ができるアプリを作ってみた
はじめに
AIは日常の様々な場面で役に立ちますが、私は特に語学学習において便利であると感じています。過去にも英語日記をAIに添削してもらいWordPressに投稿するという記事を投稿しました。普段においても、ChatGPTを使って雑に英会話の練習をしたり、わからない単語に出会ったときには意味はもちろん、用例や類似する単語とのニュアンスの違いも解説してくれます。
今回は、AIに画像を生成してもらい、画像についての英語での描写をAIに添削してもらうようなアプリを作成してみました。
成果物
最終的に出来上がるのは以下のようなアプリです。
初期状態です。
「画像を生成」ボタンをクリックすると、お題となる画像と、入力欄が表示されます。入力欄には画像を英語で描写したものを入力します。
「回答をチェック」を押すと、AIの添削結果が表示されます。
前提
- Windows10
- Python 3.12.1
- OpenAIのAPIキーの取得や環境変数の設定などは終わっているものとします
- ライブラリのインストール手順は省略します
画像の生成
画像の生成はdall-e-2
を使って行いました。執筆時点でdall-e-3
が最新ですが、私のプロンプトの問題なのか、dall-e-2
で生成した画像の方が私の希望に近かったためです。
画像の生成を行う関数のコードは以下のようになります。生成した画像のURLが返ってきますが、描写するためには目で確認する必要があるため、ファイルに保存するようにしています。
get_problem
関数の引数はどんな画像を生成するかを指定できるようにしたもので、例えば「学生生活」、「ビジネス」、「自然」などのキーワードをイメージしています。
from openai import OpenAI
import requests
openai_api_key = os.getenv("OPENAI_API_KEY")
client = OpenAI(api_key=openai_api_key)
def get_problem(theme):
generated = client.images.generate(
model="dall-e-2",
prompt=f"{theme}の一場面を写した写真を生成してください。",
size="512x512",
quality="standard",
n=1,
)
data = requests.get(generated.data[0].url)
with open("image.jpg", "wb") as f:
f.write(data.content)
return generated.data[0].url
AIによる添削
保存された画像を見て、ユーザはその画像を英語で描写します。正しく描写できているか、文章に誤りがないかなどをAIにチェックしてもらう関数は以下のようになります。
check_answer
関数の引数はそれぞれ生成された画像のURLと、ユーザが描写した英文です。
image_url
はuser
ロールでしか使えないので注意してください。
def check_answer(image_url, answer):
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{
"role": "user",
"content": [
{
"type": "text",
"text": f"私は画像を英文で描写する問題に取り組んでいます。入力された画像に対して私は{answer}と描写しました。正しく描写できているかどうか、英文に誤りがないかなどをチェックし、日本語で解説してください。",
},
{
"type": "image_url",
"image_url": {
"url": image_url,
},
},
],
}
],
)
return response.choices[0].message.content
一般的にプロンプトは英語の方が少ないトークン数で済みます。今回はわかりやすさのために日本語にしていますが、コストが気になる場合は英語に翻訳した方が良いかもしれません。
実行してみる
ここまで画像の生成、AIによる添削の二つの関数を作成したので、実際に呼び出してみます。二つの関数の定義より下に、以下のコードを記述します。
image_url = get_problem("学生生活")
my_answer = input(
"フォルダに生成されたimage.jpgを見て、画像を英文で描写してください。: "
)
ai_comment = check_answer(image_url=image_url, answer=my_answer)
print(ai_comment)
ファイルを実行します。
python main.py
実行後しばらくすると、コードファイルと同じ階層にimage.jpg
が作成されます。
コンソールに指示が表示されるので、指示通り英語で描写したものを入力します。
フォルダに生成されたimage.jpgを見て、画像を英文で描写してください。: A girl is studying. She looks like struggling with her studies.
Enterを押すと、しばらくしてAIによる添削結果が表示されます。
あなたの描写「A girl is studying. She looks like struggling with her studies.」はおおむね正しいですが、少し改善できます。以下に詳しく説明します。
1. "like" の使い方: 英文では、「looks like」の後には通常名詞か代名詞を使用します。動詞が続く場合は、「looks as if」や「looks like she is」を使うのが一般的です。
2. 冗長性: 2つの文の間により良い接続を考慮すると、文が滑らかになります。
したがって、より適切な英訳は以下のようになります:
"A girl is studying. She looks as if she is struggling with her studies."
日本語で説明すると、以下のようなポイントに気をつけて改善された英文を使います:
1. 最初の「like」の使い方を修正し、「She looks as if she is」を使う。
2. 文をよりスムーズに繋げるために、完全な表現に変更する。
その結果、次のような改善された英文が得られます:
"A girl is studying. She looks as if she is struggling with her studies."
いい感じですね。
試しに誤った描写をしてみます。
フォルダに生成されたimage.jpgを見て、画像を英文で描写してください。: There are many boys. They are playing baseball.
この画像を基にした描写「There are many boys. They are playing baseball.」は正しくありません。
画像には、1人の女の子が写っています。彼女は制服を着ていて、視線を横に向けているようです。「There are many boys. They are playing baseball.」の文は、この画像の状況を正確に反映して
いません。
より適切な英文の描写例としては:
「There is a girl wearing a uniform. She is looking to the side.」
が考えられます。
日本語での解説:
- 「There are many boys.」は「たくさんの男の子がいます」という意味ですが、この画像には男の子たちは描かれていません。
- 「They are playing baseball.」は「彼らは野球をしています」という意味ですが、画像の女の子は野球をしていません。
- よって、正確な描写は「制服を着た女の子がいます。彼女は横を向いています。」といった説明が適しています。
こちらもいい感じですね。
ここまでのコードは以下の通りです。
from openai import OpenAI
import os
import requests
openai_api_key = os.getenv("OPENAI_API_KEY")
client = OpenAI(api_key=openai_api_key)
def get_problem(theme):
generated = client.images.generate(
model="dall-e-2",
prompt=f"{theme}の一場面を写した写真を生成してください。",
size="512x512",
quality="standard",
n=1,
)
data = requests.get(generated.data[0].url)
with open("image.jpg", "wb") as f:
f.write(data.content)
return generated.data[0].url
def check_answer(image_url, answer):
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{
"role": "user",
"content": [
{
"type": "text",
"text": f"私は画像を英文で描写する問題に取り組んでいます。入力された画像に対して私は{answer}と描写しました。正しく描写できているかどうか、英文に誤りがないかなどをチェックし、日本語で解説してください。",
},
{
"type": "image_url",
"image_url": {
"url": image_url,
},
},
],
}
],
)
return response.choices[0].message.content
image_url = get_problem("学生生活")
my_answer = input(
"フォルダに生成されたimage.jpgを見て、画像を英文で描写してください。: "
)
ai_comment = check_answer(image_url=image_url, answer=my_answer)
print(ai_comment)
これで完成…と言いたいところですが、コンソールでいちいち実行するのは少し手間ですよね。できればUIが欲しいところです。
というわけで続いてこのアプリにUIをつけていきます。
UIの作成
といっても、UIを作るのは大変そうですよね。Webアプリにするのだったら、HTMLやCSSも書かないといけない、と思われるかもしれません。しかも、せっかくここまでPythonコードを書いてアプリが出来上がったのに、また大幅な変更を加えないといけないかもしれない、とも思われるかもしれません。
そこで今回はStreamlitというPythonのライブラリを使います。
Streamlit • A faster way to build and share data apps
このライブラリを簡単に説明すると、
- PythonコードだけでフロントエンドのUIが作れる
- HTML、CSSは一切不要
- GitHubリポジトリから公式のクラウドサービスに無料で爆速デプロイできる
というものです。
百聞は一見にしかずということで、まずはライブラリをインストールします。
pip install streamlit
Streamlitのコマンドは以下の通りです。
streamlit run main.py
とりあえず現在のファイルのまま上記コマンドを実行してみます。ローカルのWebサーバーが立ち上がり、自動的にブラウザで表示されます。今は何もしていないので、何も表示されません。
※ちなみに処理はちゃんと動いているので、コンソールに「フォルダに生成されたimage.jpgを見て、画像を英文で描写してください。:」という文章が表示され、image.jpg
も生成されています。
Ctrl + C
で実行を止めます。
ではまず、このWebアプリにタイトルをつけてみます。二つの関数の定義が終わり、get_problem
関数を呼び出す手前に1行コードを追加します。
st.title("画像を英文で描写する問題ジェネレーター") # これを追加
image_url = get_problem("学生生活")
my_answer = input(
"フォルダに生成されたimage.jpgを見て、画像を英文で描写してください。: "
実行すると、タイトルがつきました。
続いて、生成する画像のテーマを選べるようにしてみます。タイトルの下にコードを追加します。
st.title("画像を英文で描写する問題ジェネレーター")
theme = st.selectbox(
"テーマを選択してください",
["学生生活", "旅行", "仕事", "スポーツ", "自然"],
)
実行すると、テーマをセレクトボックスで選べるようになりました。
もうおわかりでしょうか?このように、少しのPythonコードを書くだけで、HTMLやCSSを一切書かなくても上記のようなリッチなUIを自動で作成してくれるのがStreamlitです。
さて、続いてボタンを押したらAIに画像を生成させるようにしてみます。
if st.button("画像を生成"):
with st.spinner("画像を生成中..."):
image_url = get_problem(theme)
st.image(image_url)
実行すると、ボタンが表示されます。
ボタンを押すと、ローディング中のメッセージが表示されます。
AIが画像を生成し終えると、画像が表示されます。
続いてユーザがこの画像を元に英文を入力できるように、テキストエリアを作成します。
if st.button("画像を生成"):
with st.spinner("画像を生成中..."):
image_url = get_problem(theme)
st.image(image_url)
st.write("生成された画像を見て、画像を英文で描写してください。")
my_answer = st.text_area("英文の回答", height=200)
画像の生成が完了すると、画像の下に入力欄が表示されます。
以下のコードはもう不要なので削除します。
my_answer = input(
"フォルダに生成されたimage.jpgを見て、画像を英文で描写してください。: "
)
また、get_problem
関数内でimage.jpg
を保存している箇所も不要なので削除します。
この画像と入力内容を元にAIに添削してもらうよう、もう一つボタンを作成します。まずは、先ほど作成したボタンと同じようにコードを書いてみます。
if st.button("画像を生成"):
with st.spinner("画像を生成中..."):
image_url = get_problem(theme)
st.image(image_url)
st.write("生成された画像を見て、画像を英文で描写してください。")
my_answer = st.text_area("英文の回答", height=200)
if st.button("回答をチェック"):
with st.spinner("チェック中..."):
ai_comment = check_answer(image_url=image_url, answer=my_answer)
st.write(ai_comment)
見た目はいい感じですね。
しかし、「回答をチェック」ボタンを押すと、画像やテキストエリアが消えて初期状態に戻ってしまいます。
実はStreamlitは、ボタンを押すなどウィジェットに何か変更があるたびにアプリが再ロードされます。そのため、「回答をチェック」ボタンを押すと再ロードされ、「画像を生成」ボタンを押した後に表示される内容が消えてしまいます。
そこで、再ロードされても内容を維持できるように、session_state
という機能を使います。通常の変数はアプリが再ロードすると初期状態に戻ってしまいますが、session_state
に保存した内容は再ロードしても値を維持することができます。
Session State - Streamlit Docs
まず、session_stateの状態を初期化します。
# 初期化
if "image_url" not in st.session_state:
st.session_state.image_url = ""
if "my_answer" not in st.session_state:
st.session_state.my_answer = ""
続いて、コードを以下のように修正します。
if st.button("画像を生成"):
# 画像を生成ボタンを押されたら、画像も回答もクリアする
st.session_state.image_url = ""
st.session_state.my_answer = ""
with st.spinner("画像を生成中..."):
# 画像をセッションに保存
st.session_state.image_url = get_problem(theme)
# 画像がセッションに存在したら、画像と回答用テキストエリアを表示する
if st.session_state.image_url != "":
# セッションに保存されている画像を表示する
st.image(st.session_state.image_url)
st.write("生成された画像を見て、画像を英文で描写してください。")
# 回答をセッションに保存
st.session_state.my_answer = st.text_area("英文の回答", height=200)
if st.button("回答をチェック"):
with st.spinner("チェック中..."):
ai_comment = check_answer(
image_url=st.session_state.image_url, answer=st.session_state.my_answer
)
st.write(ai_comment)
いきなりコード量が増えて戸惑うかもしれません。セッション管理は少しだけ慣れが必要です。しかし、基本的には以下の点をおさえておけばまずは大丈夫です。
- ずっと維持しておきたい値を
session_state
に保存しておく session_state
に値があるかどうかで表示するウィジェットを制御する- 必要なタイミングで
session_state
をクリアする
今回の場合は、「画像のURL」と「ユーザの回答」さえあれば、AIに添削してもらうことができます。そのためその二つをsession_state
に保存しておきます。そして、画像と回答欄が表示されるのは画像のURLが存在しているときです。
このように変更することで、「回答をチェック」を押しても初期状態に戻らず、AIの添削まで表示することができました。
コード全体は以下のようになります。
from openai import OpenAI
import os
import streamlit as st
openai_api_key = os.getenv("OPENAI_API_KEY")
client = OpenAI(api_key=openai_api_key)
# 初期化
if "image_url" not in st.session_state:
st.session_state.image_url = ""
if "my_answer" not in st.session_state:
st.session_state.my_answer = ""
def get_problem(theme):
generated = client.images.generate(
model="dall-e-2",
prompt=f"{theme}の一場面を写した写真を生成してください。",
size="512x512",
quality="standard",
n=1,
)
return generated.data[0].url
def check_answer(image_url, answer):
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{
"role": "user",
"content": [
{
"type": "text",
"text": f"私は画像を英文で描写する問題に取り組んでいます。入力された画像に対して私は{answer}と描写しました。正しく描写できているかどうか、英文に誤りがないかなどをチェックし、日本語で解説してください。",
},
{
"type": "image_url",
"image_url": {
"url": image_url,
},
},
],
}
],
)
return response.choices[0].message.content
st.title("画像を英文で描写する問題ジェネレーター")
theme = st.selectbox(
"テーマを選択してください",
["学生生活", "旅行", "仕事", "スポーツ", "自然"],
)
if st.button("画像を生成"):
# 画像を生成ボタンを押されたら、画像も回答もクリアする
st.session_state.image_url = ""
st.session_state.my_answer = ""
with st.spinner("画像を生成中..."):
# 画像をセッションに保存
st.session_state.image_url = get_problem(theme)
# 画像がセッションに存在したら、画像と回答用テキストエリアを表示する
if st.session_state.image_url != "":
# セッションに保存されている画像を表示する
st.image(st.session_state.image_url)
st.write("生成された画像を見て、画像を英文で描写してください。")
# 回答をセッションに保存
st.session_state.my_answer = st.text_area("英文の回答", height=200)
if st.button("回答をチェック"):
with st.spinner("チェック中..."):
ai_comment = check_answer(
image_url=st.session_state.image_url, answer=st.session_state.my_answer
)
st.write(ai_comment)
デプロイする
このままでは毎回ローカルで起動する必要があります。そこで、Streamlitの公式クラウドサービスにデプロイしてどこからでもアクセスできるようにしてみます。
その前に、今はOpenAIのAPIキーを環境変数から取得するようになっています。デプロイするにあたってシークレットを登録する手順がありますが、Streamlitのシークレットから値を取得するには独自の書き方があります。
APIキーを取得する部分を以下のように変更します。st.secrets
がStreamlitのシークレットから値を取得するための書き方です。
openai_api_key = os.getenv("OPENAI_API_KEY") or st.secrets["openai_api_key"]
また、requirements.txtを作成します。
pip freeze > requirements.txt
作成したコードをGitHubのリポジトリにpushします。
続いてこちらのURLからアカウント登録します。
アカウント登録が終わったら、右上の「Create app」をクリックします。
既に作成したアプリをデプロイするので、左の「Yup, I have an app」を選択します。
下記のような画面になるので、リポジトリ、ブランチ、ファイルを選択します。App URLはデフォルトのままでも、好きな文字列に変えても大丈夫です。
今回はシークレットの登録があるので、デプロイする前に「Advanced settings」をクリックします。
Pythonバージョンとシークレットの設定を行います。
デプロイすると、完了するまでポップなローディング画面が表示されます。
完了するとWeb画面が表示され、全世界のどこからでもアクセスできるようになりました。(この画像のどのあたりがスポーツなのかよくわかりませんが)
ちなみに自分のOpenAIのAPIキーを使用する場合、URLを不特定多数に公開してしまうと思わぬ請求が発生する可能性があるので注意してください。あくまで自分用として使うか、OpenAI側で制限をかけるなどの対策が必要になります。
また、APIキーをアプリ内で設定せずに、都度入力させるような方式もアリかなと思います。
この場合のコードは以下になります。なんと1行変更するだけです。
api_key = st.sidebar.text_input("OpenAI API Key", type="password")
client = OpenAI(api_key=api_key)
アプリを修正したい場合は、GitHubのリポジトリにpushすればほぼリアルタイムで反映されます。再デプロイのために何か手動で作業を行う必要はありません。
ちなみにこのクラウドサービスは無料なので、リソース制限があります。また、しばらくアクセスしていないと次回アクセスした際に少し待機時間が入ります。そういった挙動が気になる場合は、Dockerなど他にもデプロイ手段があるので、調べてみてください。
Deployment tutorials - Streamlit Docs
おわりに
使用したAIモデルやプロンプトの内容はお好みでカスタマイズして頂ければと思います。AIなのでたまに変な画像が生成される場合もありますが、英文描写力を鍛えるアプリなので良いのではないでしょうか。
今後もAIを使って語学学習を便利にする仕組みを考案していけたらと思います。
この記事がどなたかの参考になれば幸いです。